#if ENABLE_ANIMATION_COLLECTION && ENABLE_ANIMATION_BURST
#define ENABLE_ANIMATION_PERFORMANCE
#endif

using UnityEngine;
using UnityEditorInternal;
using UnityEngine.U2D.Animation;
using UnityEditor.IMGUI.Controls;
using UnityEngine.U2D;

namespace UnityEditor.U2D.Animation
{
    [CustomEditor(typeof(SpriteSkin))]
    [CanEditMultipleObjects]
    class SpriteSkinEditor : Editor
    {
        private static class Contents
        {
            public static readonly GUIContent listHeaderLabel = new GUIContent("Bones", "GameObject Transform to represent the Bones defined by the Sprite that is currently used for deformation.");
            public static readonly GUIContent rootBoneLabel = new GUIContent("Root Bone", "GameObject Transform to represent the Root Bone.");
            public static readonly string spriteNotFound = L10n.Tr("Sprite not found in SpriteRenderer");
            public static readonly string spriteHasNoSkinningInformation = L10n.Tr("Sprite has no Bind Poses");
            public static readonly string spriteHasNoWeights = L10n.Tr("Sprite has no weights");
            public static readonly string rootTransformNotFound = L10n.Tr("Root Bone not set");
            public static readonly string rootTransformNotFoundInArray = L10n.Tr("Bone list doesn't contain a reference to the Root Bone");
            public static readonly string invalidTransformArray = L10n.Tr("Bone list is invalid");
            public static readonly string transformArrayContainsNull = L10n.Tr("Bone list contains unassigned references");
            public static readonly string invalidTransformArrayLength = L10n.Tr("The number of Sprite's Bind Poses and the number of Transforms should match");
            public static readonly GUIContent useManager = new GUIContent("Enable batching", "When enabled, SpriteSkin deformation will be done in batch to improve performance.");
            public static readonly GUIContent alwaysUpdate = new GUIContent("Always Update", "Executes deformation of SpriteSkin even when the associated SpriteRenderer has been culled and is not visible.");
            public static readonly GUIContent autoRebind = new GUIContent("Auto Rebind", "When the Sprite in SpriteRenderer is changed, SpriteSkin will try to look for the Transforms that is needed for the Sprite using the Root Bone Tranform as parent.");    
            public static readonly string enableBatchingHelp = L10n.Tr("Install Burst and Collection package to enable deformation batching.");
        }

        private static Color s_BoundingBoxHandleColor = new Color(255, 255, 255, 150) / 255;

        private SerializedProperty m_RootBoneProperty;
        private SerializedProperty m_BoneTransformsProperty;
        private SerializedProperty m_AlwaysUpdateProperty;
        private SerializedProperty m_AutoRebindProperty;
        private SpriteSkin m_SpriteSkin;
        private ReorderableList m_ReorderableList;
        private Sprite m_CurrentSprite;
        private BoxBoundsHandle m_BoundsHandle = new BoxBoundsHandle();
        private bool m_NeedsRebind = false;
        private SerializedProperty m_UseBatching;
        private bool m_BoneFold = true;

        private void OnEnable()
        {
            m_SpriteSkin = (SpriteSkin)target;
            m_SpriteSkin.OnEditorEnable();

            m_RootBoneProperty = serializedObject.FindProperty("m_RootBone");
            m_UseBatching = serializedObject.FindProperty("m_UseBatching");
            
            m_BoneTransformsProperty = serializedObject.FindProperty("m_BoneTransforms");
            m_AlwaysUpdateProperty = serializedObject.FindProperty("m_AlwaysUpdate");
            m_AutoRebindProperty = serializedObject.FindProperty("m_AutoRebind");
            
            m_CurrentSprite = m_SpriteSkin.spriteRenderer.sprite;
            m_BoundsHandle.axes = BoxBoundsHandle.Axes.X | BoxBoundsHandle.Axes.Y;
            m_BoundsHandle.SetColor(s_BoundingBoxHandleColor);

            SetupReorderableList();

            Undo.undoRedoPerformed += UndoRedoPerformed;
        }

        private void OnDestroy()
        {
            Undo.undoRedoPerformed -= UndoRedoPerformed;
        }

        private void UndoRedoPerformed()
        {
            m_CurrentSprite = m_SpriteSkin.spriteRenderer.sprite;
        }

        private void SetupReorderableList()
        {
            m_ReorderableList = new ReorderableList(serializedObject, m_BoneTransformsProperty, false, true, false, false);
            m_ReorderableList.headerHeight = 1.0f;
            m_ReorderableList.elementHeightCallback = (int index) =>
                {
                    return EditorGUIUtility.singleLineHeight + 6;
                };
            m_ReorderableList.drawElementCallback = (Rect rect, int index, bool isactive, bool isfocused) =>
                {
                    var content = GUIContent.none;

                    if (m_CurrentSprite != null)
                    {
                        var bones = m_CurrentSprite.GetBones();
                        if (index < bones.Length)
                            content = new GUIContent(bones[index].name);
                    }

                    rect.y += 2f;
                    rect.height = EditorGUIUtility.singleLineHeight;
                    SerializedProperty element = m_BoneTransformsProperty.GetArrayElementAtIndex(index);
                    EditorGUI.PropertyField(rect, element, content);
                };
        }

        private void InitializeBoneTransformArray()
        {
            if (m_CurrentSprite)
            {
                var elementCount = m_BoneTransformsProperty.arraySize;
                var bones = m_CurrentSprite.GetBones();

                if (elementCount != bones.Length)
                {
                    m_BoneTransformsProperty.arraySize = bones.Length;

                    for (int i = elementCount; i < m_BoneTransformsProperty.arraySize; ++i)
                        m_BoneTransformsProperty.GetArrayElementAtIndex(i).objectReferenceValue = null;

                    m_NeedsRebind = true;
                }
            }
        }

        public override void OnInspectorGUI()
        {
            serializedObject.Update();
            EditorGUILayout.PropertyField(m_AlwaysUpdateProperty, Contents.alwaysUpdate);
            EditorGUI.BeginChangeCheck();
            EditorGUILayout.PropertyField(m_AutoRebindProperty, Contents.autoRebind);
            if (EditorGUI.EndChangeCheck() && m_AutoRebindProperty.boolValue)
            {
                m_NeedsRebind = true;
            }
            
            var sprite = m_SpriteSkin.spriteRenderer.sprite;
            var spriteChanged = m_CurrentSprite != sprite;

            if (m_ReorderableList == null || spriteChanged)
            {
                m_CurrentSprite = sprite;
                InitializeBoneTransformArray();
                SetupReorderableList();
            }

            EditorGUI.BeginChangeCheck();
            EditorGUILayout.PropertyField(m_RootBoneProperty, Contents.rootBoneLabel);
            if (EditorGUI.EndChangeCheck())
            {
                m_NeedsRebind = true;
            }

            m_BoneFold = EditorGUILayout.Foldout(m_BoneFold, Contents.listHeaderLabel, true);
            if (m_BoneFold)
            {
                EditorGUILayout.Space();
                if (!serializedObject.isEditingMultipleObjects)
                {
                    EditorGUI.BeginDisabledGroup(m_SpriteSkin.rootBone == null);
                    m_ReorderableList.DoLayoutList();
                    EditorGUI.EndDisabledGroup();
                }
            }

            EditorGUILayout.BeginHorizontal();
            GUILayout.FlexibleSpace();

            EditorGUI.BeginDisabledGroup(!EnableCreateBones());
            DoGenerateBonesButton();
            EditorGUI.EndDisabledGroup();

            EditorGUI.BeginDisabledGroup(!EnableSetBindPose());
            DoResetBindPoseButton();
            EditorGUI.EndDisabledGroup();

            GUILayout.FlexibleSpace();
            EditorGUILayout.EndHorizontal();

#if !ENABLE_ANIMATION_PERFORMANCE
            EditorGUILayout.HelpBox(Contents.enableBatchingHelp, MessageType.Info);
            using (new EditorGUI.DisabledScope(true))
#endif
            {
                EditorGUI.BeginChangeCheck();
                EditorGUILayout.PropertyField(m_UseBatching, Contents.useManager);
                if (EditorGUI.EndChangeCheck())
                {
                    foreach (var obj in targets)
                    {
                        ((SpriteSkin)obj).UseBatching(m_UseBatching.boolValue);
                    }
                }   
            }

            serializedObject.ApplyModifiedProperties();

            if (m_NeedsRebind)
                Rebind();

            if (spriteChanged && !m_SpriteSkin.ignoreNextSpriteChange)
            {
                ResetBounds(Undo.GetCurrentGroupName());
                m_SpriteSkin.ignoreNextSpriteChange = false;
            }

            DoValidationWarnings();
        }

        private void Rebind()
        {
            foreach (var t in targets)
            {
                var spriteSkin = t as SpriteSkin;

                if(spriteSkin.spriteRenderer.sprite == null || spriteSkin.rootBone == null)
                    continue;
                var spriteBones = spriteSkin.spriteRenderer.sprite.GetBones();
                var transforms = new Transform[spriteBones.Length];
                if (SpriteSkin.GetSpriteBonesTransforms(spriteBones, spriteSkin.rootBone, transforms))
                    spriteSkin.boneTransforms = transforms;
                
                ResetBoundsIfNeeded(spriteSkin);
            }

            serializedObject.Update();
            m_NeedsRebind = false;
        }
        
        private void ResetBounds(string undoName = "Reset Bounds")
        {
            foreach (var t in targets)
            {
                var spriteSkin = t as SpriteSkin;

                if (!spriteSkin.isValid)
                    continue;

                Undo.RegisterCompleteObjectUndo(spriteSkin, undoName);
                spriteSkin.CalculateBounds();

                EditorUtility.SetDirty(spriteSkin);
            }
        }

        private void ResetBoundsIfNeeded(SpriteSkin spriteSkin)
        {
            if (spriteSkin.isValid && spriteSkin.bounds == new Bounds())
                spriteSkin.CalculateBounds();
        }

        private bool EnableCreateBones()
        {
            foreach (var t in targets)
            {
                var spriteSkin = t as SpriteSkin;
                var sprite = spriteSkin.spriteRenderer.sprite;

                if (sprite != null && spriteSkin.rootBone == null)
                    return true;
            }
            return false;
        }

        private bool EnableSetBindPose()
        {
            return IsAnyTargetValid();
        }

        private bool IsAnyTargetValid()
        {
            foreach (var t in targets)
            {
                var spriteSkin = t as SpriteSkin;

                if (spriteSkin.isValid)
                    return true;
            }
            return false;
        }

        private void DoGenerateBonesButton()
        {
            if (GUILayout.Button("Create Bones", GUILayout.MaxWidth(125f)))
            {
                foreach (var t in targets)
                {
                    var spriteSkin = t as SpriteSkin;
                    var sprite = spriteSkin.spriteRenderer.sprite;

                    if (sprite == null || spriteSkin.rootBone != null)
                        continue;

                    Undo.RegisterCompleteObjectUndo(spriteSkin, "Create Bones");

                    spriteSkin.CreateBoneHierarchy();

                    foreach (var transform in spriteSkin.boneTransforms)
                        Undo.RegisterCreatedObjectUndo(transform.gameObject, "Create Bones");

                    ResetBoundsIfNeeded(spriteSkin);

                    EditorUtility.SetDirty(spriteSkin);
                }
                BoneGizmo.instance.boneGizmoController.OnSelectionChanged();
            }
        }

        private void DoResetBindPoseButton()
        {
            if (GUILayout.Button("Reset Bind Pose", GUILayout.MaxWidth(125f)))
            {
                foreach (var t in targets)
                {
                    var spriteSkin = t as SpriteSkin;

                    if (!spriteSkin.isValid)
                        continue;

                    Undo.RecordObjects(spriteSkin.boneTransforms, "Reset Bind Pose");
                    spriteSkin.ResetBindPose();
                }
            }
        }

        private void DoValidationWarnings()
        {
            EditorGUILayout.Space();

            bool preAppendObjectName = targets.Length > 1;

            foreach (var t in targets)
            {
                var spriteSkin = t as SpriteSkin;

                var validationResult = spriteSkin.Validate();

                if (validationResult == SpriteSkinValidationResult.Ready)
                    continue;

                var text = "";

                switch (validationResult)
                {
                    case SpriteSkinValidationResult.SpriteNotFound:
                        text = Contents.spriteNotFound;
                        break;
                    case SpriteSkinValidationResult.SpriteHasNoSkinningInformation:
                        text = Contents.spriteHasNoSkinningInformation;
                        break;
                    case SpriteSkinValidationResult.SpriteHasNoWeights:
                        text = Contents.spriteHasNoWeights;
                        break;
                    case SpriteSkinValidationResult.RootTransformNotFound:
                        text = Contents.rootTransformNotFound;
                        break;
                    case SpriteSkinValidationResult.RootNotFoundInTransformArray:
                        text = Contents.rootTransformNotFoundInArray;
                        break;
                    case SpriteSkinValidationResult.InvalidTransformArray:
                        text = Contents.invalidTransformArray;
                        break;
                    case SpriteSkinValidationResult.InvalidTransformArrayLength:
                        text = Contents.invalidTransformArrayLength;
                        break;
                    case SpriteSkinValidationResult.TransformArrayContainsNull:
                        text = Contents.transformArrayContainsNull;
                        break;
                }

                if (preAppendObjectName)
                    text = string.Format("{0}:{1}",spriteSkin.name,text);

                EditorGUILayout.HelpBox(text, MessageType.Warning);
            }
        }
    }
}
